[原创]AUPK:基于Art虚拟机的脱壳机 |
您所在的位置:网站首页 › android 脱壳机 › [原创]AUPK:基于Art虚拟机的脱壳机 |
一. 背景 如今,随着开发者安全意识的提高,加壳的app比比皆是,反而不加壳的并不多见.对于一款加壳的app,脱壳是逆向工程中非常重要的一个步骤.有一个好的脱壳机,可以大大节省逆向人员的时间与精力. android系统的换代升级非常块,在4.4版本后舍弃了以往的dalvik虚拟机,取而代之的是更高效的art虚拟机.在android脱壳领域,有许多前辈共享了其思路,代码,使本人得已站在巨人的肩膀上进行学习,研究.如FART:ART环境下基于主动调用的自动化脱壳方案中,hanbingle大佬开源的基于art的主动调用方案:FART. 那么,既然有了FART,为什么我还要写AUPK呢?FART是通过ClassLoader里的path_list来获取DexFile对象,再通过以下方法来获取到所有类, 12package dalvik.system.DexFile;String[] getClassNameList(DexFile dexFile,Object mCookie);进而获取到所有Method对象并进行主动调用.这种方案在大多数情况下非常有效,但对某些壳,如某南极动物壳,这里无法获取到关键的DexFile对象,导致无法进行后面的主动调用.AUPK提供了其他思路来应对这种情况. FART的调用链只到了Invoke函数即返回,对于许多函数恢复时机更晚的抽取壳,如某加密,某叠字壳,如果函数是第一次执行,此时dump下来的codeItem,显然是未解密的,AUPK构造了更深的调用链,以及更晚的dump时机来解决这一问题. 论坛也有许多其他的大佬,提供了ART下的主动调用脱壳思路,但遗憾的是,均未开源。所以我决定自己实现一个脱壳机。 二. 原理整体流程图: 脱壳 关于如何整体dump Dex文件,网上有诸多的解决方案,各家都有不同的脱壳点,如DexFile:init,DexFile::OpenCommon等,其原理是因为dex文件在进行加载的时候,必定会调用这些函数,且此时能方便的拿到DexFile对象,从而进行dump。AUPK的脱壳点在ExecuteSwitchImpl,当满足特定条件时,类中的某些函数必定会走到这里,此时可以通过栈帧拿到ArtMethod对象,进而获取到DexFile对象。 在拨云见日:安卓APP脱壳的本质以及如何快速发现ART下的脱壳点一文中,作者阐明了,不管壳是否禁用了dex2oat,类的初始化函数始终运行在ART下的inpterpreter模式,即class.函数是必定要运行到Execute函数中的,当app运行时,不可避免的要加载dex,加载、链接、解析、初始化类,而当有类进行初始化的时候,就一定会走到Execute Execute部分代码如下所示: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374// interpreter.ccstatic inline JValue Execute( Thread *self, const DexFile::CodeItem *code_item, ShadowFrame &shadow_frame, JValue result_register, bool stay_in_interpreter = false) SHARED_REQUIRES(Locks::mutator_lock_){ if (kInterpreterImplKind == kMterpImplKind) { if (transaction_active) { // No Mterp variant - just use the switch interpreter. return ExecuteSwitchImpl(self, code_item, shadow_frame, result_register, false); } else if (UNLIKELY(!Runtime::Current()->IsStarted())) { return ExecuteSwitchImpl(self, code_item, shadow_frame, result_register, false); } else { while (true) { // Mterp does not support all instrumentation/debugging. if (MterpShouldSwitchInterpreters()) { return ExecuteSwitchImpl(self, code_item, shadow_frame, result_register,false); } bool returned = ExecuteMterpImpl(self, code_item, &shadow_frame, &result_register); if (returned) { return result_register; } else { // Mterp didn't like that instruction. Single-step it with the reference interpreter. result_register = ExecuteSwitchImpl(self, code_item, shadow_frame,result_register, true); if (shadow_frame.GetDexPC() == DexFile::kDexNoIndex) { // Single-stepped a return or an exception not handled locally. Return to caller. return result_register; } } } } } else if (kInterpreterImplKind == kSwitchImplKind) { if (transaction_active) { return ExecuteSwitchImpl(self, code_item, shadow_frame, result_register, false); } else { return ExecuteSwitchImpl(self, code_item, shadow_frame, result_register, false); } } else { DCHECK_EQ(kInterpreterImplKind, kComputedGotoImplKind); if (transaction_active) { return ExecuteGotoImpl(self, code_item, shadow_frame, result_register); } else { return ExecuteGotoImpl(self, code_item, shadow_frame, result_register); } } ... }可以看到,Execute会根据当前选择的解释器类型,进入不同的解释器去执行,当我们手动把默认的解释器改为kSwitchImplKind,则可以让Execute默认走swich型解释器,从而进入ExecuteSwitchImpl 123// interpreter.cc//static constexpr InterpreterImplKind kInterpreterImplKind = kMterpImplKind;static constexpr InterpreterImplKind kInterpreterImplKind = kSwitchImplKind;在ExecuteSwitchImpl函数中,通过shadow_frame.GetMethod()可以拿到当前ArtMethod对象,再通过artMethod->GetDexFile()可以获取DexFile对象, 最后通过DexFile对象的Begin(),Size()获取到起始位置和大小,来dump Dex文件. 123456789101112131415template JValue ExecuteSwitchImpl(Thread *self, const DexFile::CodeItem *code_item, ShadowFrame &shadow_frame, JValue result_register, bool interpret_one_instruction) SHARED_REQUIRES(Locks::mutator_lock_){ ArtMethod *artMethod = shadow_frame.GetMethod(); bool isFakeInvokeMethod = Aupk::isFakeInvoke(self, artMethod); if (!isFakeInvokeMethod && strstr(PrettyMethod(shadow_frame.GetMethod()).c_str(), "") != nullptr) { const DexFile *dexFile = artMethod->GetDexFile(); char feature[] = "ExecuteSwitchImpl"; Aupk::dumpDexFile(dexFile, feature); Aupk::dumpClassName(dexFile, feature); }}与此同时,在dumpClassName函数中,获取Dex文件的所有类名,并写入到文件(关于这一步的作用,后面会讲到): 12345678910111213141516json root;for (size_t i = 0; i < dexFile->NumClassDefs(); i++){ const DexFile::ClassDef &classDef = dexFile->GetClassDef(i); const char *descriptor = dexFile->GetClassDescriptor(classDef); root["count"] = i + 1; root["data"][i] = descriptor;}ofstream oFile;oFile.open(fileName, ios::out);if (oFile.is_open()){ oFile IsNative() || (!artMethod->IsInvokable()) || artMethod->IsProxyMethod()) { return; } JValue result; Thread *self = Thread::Current(); uint32_t args_size = (uint32_t)ArtMethod::NumArgRegisters(artMethod->GetShorty()); if (!artMethod->IsStatic()) { args_size += 1; } std::vector args(args_size, 0); if (!artMethod->IsStatic()) { args[0] = 0x12345678; } artMethod->Invoke(self, args.data(), args_size, &result, artMethod->GetShorty());}在解释执行模式下,Native方法和动态Proxy方法没有意义,所以直接跳过,抽象方法不能执行,需要其子类提供实现,所以也直接跳过 需要注意的是,函数的第一个参数要赋值为除0以外的任何值,因为后面会判断 CHECK(arg[0]!=nullptr) 在art中,系统会在app安装或运行过程中对dex文件进行dex2oat,将其中的函数编译成目标代码,而不再是虚拟机执行的字节码,同时也保留了原来由虚拟机执行的字节码,所以art代码既可以运行在解释器模式下,也可以作为quick_code来执行。为了使主动调用的函数能够到达ExecuteSwitchImpl函数,这里使主动调用的函数强制走解释器: 12345678910111213141516171819202122232425262728293031323334//art_method.ccvoid ArtMethod::Invoke(Thread *self, uint32_t *args, uint32_t args_size, JValue *result,const char *shorty){ ... bool isFakeInvokeMethod = Aupk::isFakeInvoke(self, this); if (UNLIKELY(!runtime->IsStarted() || Dbg::IsForcedInterpreterNeededForCalling(self, this) || isFakeInvokeMethod)) { if (IsStatic()) { art::interpreter::EnterInterpreterFromInvoke( self, this, nullptr, args, result, /*stay_in_interpreter*/ true); } else { mirror::Object *receiver = reinterpret_cast(&args[0])->AsMirrorPtr(); art::interpreter::EnterInterpreterFromInvoke( self, this, receiver, args + 1, result, /*stay_in_interpreter*/ true); } } else { ... if (!IsStatic()) { (*art_quick_invoke_stub)(this, args, args_size, self, result, shorty); } else { (*art_quick_invoke_static_stub)(this, args, args_size, self, result, shorty); } ... }}这样一来,主动调用时,函数不管是静态还是非静态,都会走art::interpreter::EnterInterpreterFromInvoke,分析EnterInterpreterFromInvoke函数可知,如果调用的方法为非native方法,则会进入Execute函数继续执行,而native方法,并非我们所关注的对象,所以在主动调用前,会过滤掉native函数 123456789101112131415161718void EnterInterpreterFromInvoke(Thread *self, ArtMethod *method, Object *receiver, uint32_t *args, JValue *result, bool stay_in_interpreter){ ... if (LIKELY(!method->IsNative())) { JValue r = Execute(self, code_item, *shadow_frame, JValue(), stay_in_interpreter); if (result != nullptr) { *result = r; } } else { ... } ...}如上文所述,进入Execute后,函数最终会进入ExecuteSwitchImpl执行。在ExecuteSwitchImpl函数中,准备工作都已做好,函数指令即将开始执行,对于大多数壳来说,此时函数的code_item都已修复,否则函数无法正常执行,这也是为什么会将主动调用链构造到这里的原因。事实上,对于大多数壳,在这里都可以获取到已被恢复的code_item,个别情况,如在函数体内部goto跳转进行自解密的情况,则需要单独处理: 123456789101112131415161718192021222324252627282930313233343536bool firstInsIsGoto = false;if (isFakeInvokeMethod){ inst_data = inst->Fetch16(0); // 当执行的为方法的第一条指令时 if (dex_pc == 0) { if (inst->Opcode(inst_data) == Instruction::GOTO || inst->Opcode(inst_data) == Instruction::GOTO_16 || inst->Opcode(inst_data) == Instruction::GOTO_32) { // 如果第一条指令为goto,则继续执行 firstInsIsGoto = true; } else { // 如果第一条指令不是goto,则直接dumpMethod char feature[] = "ExecuteSwitchImpl"; Aupk::dumpMethod(method, feature); return JValue(); } } if(inst->Opcode(inst_data) == Instruction::INVOKE_STATIC) { if (firstInsIsGoto) { // 如果指令为invoke-static,且第一条指令为goto,则等invoke-static执行完毕后再dumpMethod DoInvoke(self, shadow_frame, inst, inst_data, &result_register); char feature[] = "ExecuteSwitchImpl"; LOG(INFO)data/local/tmp/aupk.config或者对所有的Dex里的类的所有方法进行主动调用: 1$ echo "com.klcxkj.zqxy">data/local/tmp/aupk.config主动调用过程中打印的log: 有的壳hook了Log函数,导致Log.i()打印不出来消息,但jni层的LOG和Log.e()依然有效,当打印出Aupk run over时,代表整个主动调用过程结束,可以在data/data/you_app/aupk下看到一些以method.json结尾的文件,这些文件包含了Dex文件的函数CodeItem信息,用于后面对Dex文件的修复。 并非等整个主动调用过程结束才会生成method.json文件,而是每完成对一个class.json文件的解析和调用,就会立即生成对应的method.json,所以,利用主动调用的这段时间,你可以先修复已经完成了主动调用的Dex文件,或者去泡杯咖啡。 将脱下来的文件拷贝到电脑: 1$ adb pull data/data/your_app/aupk开始修复Dex,回填CodeItem: 1$ dp fix -d 8273364_ExecuteSwitchImpl.dex -j 8273364_ExecuteSwitchImpl_method.json [--nolog]等待片刻,即可完成修复: 带patched后缀的就是修复后的Dex文件 反编译看看效果: 四.对抗手段 以下为某些场景下的对抗手段(纯YY,并未遇到过),以及个人的一些应对思路 文件检测应对方法:修改配置文件和中间文件的名字,路径。 方法,类检测 应对方法:更改类名,方法名 插入垃圾类 AUPK使用loadClass加载类,并不会触发类的函数,也就不会触发静态代码块里的检测。但在调用静态函数时,如果类未初始化,art会调用EnsureInitialized去初始化这个类,此时会调用类的初始化函数,触发静态代码块,其过程如下所示: 12345678910111213141516171819void EnterInterpreterFromInvoke(Thread* self, ArtMethod* method, Object* receiver, uint32_t* args, JValue* result) { ... // Do this after populating the shadow frame in case EnsureInitialized causes a GC. if (method->IsStatic() && UNLIKELY(!method->GetDeclaringClass()->IsInitialized())) { ClassLinker* class_linker = Runtime::Current()->GetClassLinker(); StackHandleScope hs(self); Handle h_class(hs.NewHandle(method->GetDeclaringClass())); if (UNLIKELY(!class_linker->EnsureInitialized(self, h_class, true, true))) { CHECK(self->IsExceptionPending()); self->PopShadowFrame(); return; } } ... }应对方法: 壳为了保证效率,通常在恢复函数指令后,就不会再对其进行抽取了。所以,可以等app运行到一定阶段,确保感兴趣的地方已经被执行过,就可以采用dex整体dump的方式,来dump Dex文件,此时虽然部分函数依然是抽取状态,但关注的部分函数已经解密。 在AUPK中,你可以删除掉所有的*.dex文件,然后重新操作一下app,即可完成对Dex文件的重新dump。 12$ cd data/data/your_app/aupk$ rm *.dexfrida提供了一个api:enumerateLoadedClasses,该方法可以获取到所有已加载的类,然后通过反射去获取类中的所有函数,并调用Aupk中的fakeInvoke ,进行主动调用。这样获得的类虽然不全,但包含了app正常使用中所用到的类,且避开了插入的垃圾类。该方法是在类的粒度进行处理,理论上讲,比方法1修复得更完整。伪代码如下: 12345678910111213function test() { Java.perform(function () { Java.enumerateLoadedClasses({ onMatch: function (className) { var klass = loadClass(className); var methods = klass.getMethods(); fakeInvoke(methods); }, onComplete: function () { } }) });}在fakeInvoke前,将当前方法所属的类名写入文件temp.txt,当最后一次执行fakeInvoke时,清空temp.txt.在发起主动调用前,先检查temp.txt,将其中temp.txt中的项追加到detectClass.txt,并跳过所有属于detectClass.txt中的类的函数。当壳检测到脱壳机,终止app时,temp.txt中的内容为非空。如此反复,到最后主动调用成功时,detectClass.txt中则记录着所有检测点所属的类。 五.写在最后从学习脱壳到现在,不知不觉,竟已两月有余,期间遇到过许许多多,一言难尽的问题。对于这类事物,我认为仅仅靠一两篇文章,读者依然很难去复现它。为了能让像我一样的新手去更好的学习和参考,我将同hanbingle大佬一样,将其开源。 AUPK:https://github.com/FeJQ/AUPK DexPatcher:https://github.com/FeJQ/DexPatcher 参考文献: [1] hanbingle.FART:ART环境下基于主动调用的自动化脱壳方案 [2] Youlor.Youpk: 又一款基于ART的主动调用的脱壳机 [3] 邓凡平.深入理解Android:Java虚拟机ART[M].机械工业出版社.2019.04 [4] FART 反勒索软件开发实战篇 最后于 2021-3-29 15:25 被FeJQ编辑 ,原因: 标题多了个前缀 #基础理论 #脱壳反混淆 |
今日新闻 |
推荐新闻 |
CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3 |